//
//
//  Storage.hpp
//  Clock Signal
//
//  Created by Thomas Harte on 12/02/2023.
//  Copyright © 2023 Thomas Harte. All rights reserved.
//

#pragma once

#include "LineBuffer.hpp"
#include "YamahaCommands.hpp"

#include <optional>
#include <vector>

namespace TI::TMS {

/// A container for personality-specific storage; see specific instances below.
template <Personality personality, typename Enable = void> struct Storage {
};

template <> struct Storage<Personality::TMS9918A> {
	using AddressT = uint16_t;

	void begin_line(ScreenMode, bool) {}
};

struct YamahaFetcher {
public:
	/// Describes an _observable_ memory access event. i.e. anything that it is safe
	/// (and convenient) to treat as atomic in between external slots.
	struct Event {
		/// Offset of the _beginning_ of the event. Not completely arbitrarily: this is when
		/// external data must be ready by in order to take part in those slots.
		uint16_t offset = 1368;
		enum class Type: uint8_t {
			/// A slot for reading or writing data on behalf of the CPU or the command engine.
			External,

			//
			// Sprites.
			//
			SpriteY,
			SpriteLocation,
			SpritePattern,

			//
			// Backgrounds.
			//
			Name,
			Colour,
			Pattern,
		} type = Type::External;
		uint8_t id = 0;

		constexpr Event(Type type, uint8_t id = 0) noexcept :
			type(type),
			id(id) {}

		constexpr Event() noexcept = default;
	};

	// State that tracks fetching position within a line.
	const Event *next_event_ = nullptr;

	// Sprite collection state.
	bool sprites_enabled_ = true;

protected:
	/// @return 1 + the number of times within a line that @c GeneratorT produces an event.
	template <typename GeneratorT> static constexpr size_t events_size() {
		size_t size = 0;
		for(int c = 0; c < 1368; c++) {
			const auto event_type = GeneratorT::event(c);
			size += event_type.has_value();
		}
		return size + 1;
	}

	/// @return An array of all events generated by @c GeneratorT in line order.
	template <typename GeneratorT, size_t size = events_size<GeneratorT>()>
	static constexpr std::array<Event, size> events() {
		std::array<Event, size> result{};
		size_t index = 0;
		for(int c = 0; c < 1368; c++) {
			// Specific personality doesn't matter here; both Yamahas use the same internal timing.
			const auto event = GeneratorT::event(from_internal<Personality::V9938, Clock::FromStartOfSync>(c));
			if(!event) {
				continue;
			}
			result[index] = *event;
			result[index].offset = uint16_t(c);
			++index;
		}
		result[index] = Event();
		return result;
	}

	struct StandardGenerators {
		static constexpr std::optional<Event> external_every_eight(int index) {
			if(index & 7) return std::nullopt;
			return Event::Type::External;
		}
	};

	struct RefreshGenerator {
		static constexpr std::optional<Event> event(const int grauw_index) {
			// From 0 to 126: CPU/CMD slots at every cycle divisible by 8.
			if(grauw_index < 126) {
				return StandardGenerators::external_every_eight(grauw_index - 0);
			}

			// From 164 to 1234: eight-cycle windows, the first 15 of each 16 being
			// CPU/CMD and the final being refresh.
			if(grauw_index >= 164 && grauw_index < 1234) {
				const int offset = grauw_index - 164;
				if(offset & 7) return std::nullopt;
				if(((offset >> 3) & 15) == 15) return std::nullopt;
				return Event::Type::External;
			}

			// From 1268 to 1330: CPU/CMD slots at every cycle divisible by 8.
			if(grauw_index >= 1268 && grauw_index < 1330) {
				return StandardGenerators::external_every_eight(grauw_index - 1268);
			}

			// A CPU/CMD at 1334.
			if(grauw_index == 1334) {
				return Event::Type::External;
			}

			// From 1344 to 1366: CPU/CMD slots every cycle divisible by 8.
			if(grauw_index >= 1344 && grauw_index < 1366) {
				return StandardGenerators::external_every_eight(grauw_index - 1344);
			}

			// Otherwise: nothing.
			return std::nullopt;
		}
	};

	template <bool include_sprites> struct BitmapGenerator {
		static constexpr std::optional<Event> event(const int grauw_index) {
			if(!include_sprites) {
				// Various standard zones of one-every-eight external slots.
				if(grauw_index < 124) {
					return StandardGenerators::external_every_eight(grauw_index + 2);
				}
				if(grauw_index > 1266) {
					return StandardGenerators::external_every_eight(grauw_index - 1266);
				}
			} else {
				// This records collection points for all data for selected sprites.
				// There's only four of them (each site covering two sprites),
				// so it's clearer just to be explicit.
				//
				// There's also a corresponding number of extra external slots to spell out.
				switch(grauw_index) {
					default: break;
					case 1238:	return Event(Event::Type::SpriteLocation, 0);
					case 1302:	return Event(Event::Type::SpriteLocation, 2);
					case 2:		return Event(Event::Type::SpriteLocation, 4);
					case 66:	return Event(Event::Type::SpriteLocation, 6);
					case 1270:	return Event(Event::Type::SpritePattern, 0);
					case 1338:	return Event(Event::Type::SpritePattern, 2);
					case 34:	return Event(Event::Type::SpritePattern, 4);
					case 98:	return Event(Event::Type::SpritePattern, 6);
					case 1264:	case 1330:	case 28:	case 92:
						return Event::Type::External;
				}
			}

			if(grauw_index >= 162 && grauw_index < 176) {
				return StandardGenerators::external_every_eight(grauw_index - 162);
			}

			// Everywhere else the pattern is:
			//
			//	external or sprite y, external, data block
			//
			// Subject to caveats:
			//
			//	1)	the first data block is just a dummy fetch with no side effects,
			//		so this emulator declines to record it; and
			//	2)	every fourth block, the second external is actually a refresh.
			//
			if(grauw_index >= 182 && grauw_index < 1238) {
				const int offset = grauw_index - 182;
				const int block = offset / 32;
				const int sub_block = offset & 31;

				switch(sub_block) {
					default:	return std::nullopt;
					case 0:
						if(include_sprites) {
							// Don't include the sprite post-amble (i.e. a spurious read with no side effects).
							if(block < 32) {
								return Event(Event::Type::SpriteY, uint8_t(block));
							}
						} else {
							return Event::Type::External;
						}
					break;
					case 6:
						if((block & 3) != 3) {
							return Event::Type::External;
						}
					break;
					case 12:
						if(block) {
							return Event(Event::Type::Pattern, uint8_t(block - 1));
						}
					break;
				}
			}

			return std::nullopt;
		}
	};

	struct TextGenerator {
		static constexpr std::optional<Event> event(const int grauw_index) {
			// Capture various one-in-eight zones.
			if(grauw_index < 72) {
				return StandardGenerators::external_every_eight(grauw_index - 2);
			}
			if(grauw_index >= 166 && grauw_index < 228) {
				return StandardGenerators::external_every_eight(grauw_index - 166);
			}
			if(grauw_index >= 1206 && grauw_index < 1332) {
				return StandardGenerators::external_every_eight(grauw_index - 1206);
			}
			if(grauw_index == 1336) {
				return Event::Type::External;
			}
			if(grauw_index >= 1346) {
				return StandardGenerators::external_every_eight(grauw_index - 1346);
			}

			// Elsewhere...
			if(grauw_index >= 246) {
				const int offset = grauw_index - 246;
				const int block = offset / 48;
				const int sub_block = offset % 48;
				switch(sub_block) {
					default: break;
					case 0:		return Event(Event::Type::Name, uint8_t(block));
					case 18:	return (block & 1) ? Event::Type::External : Event(Event::Type::Colour, uint8_t(block >> 1));
					case 24:	return Event(Event::Type::Pattern, uint8_t(block));
				}
			}

			return std::nullopt;
		}
	};

	struct CharacterGenerator {
		static constexpr std::optional<Event> event(const int grauw_index) {
			// Grab sprite events.
			switch(grauw_index) {
				default: break;
				case 1242:	return Event(Event::Type::SpriteLocation, 0);
				case 1306:	return Event(Event::Type::SpriteLocation, 1);
				case 6:		return Event(Event::Type::SpriteLocation, 2);
				case 70:	return Event(Event::Type::SpriteLocation, 3);
				case 1274:	return Event(Event::Type::SpritePattern, 0);
				case 1342:	return Event(Event::Type::SpritePattern, 1);
				case 38:	return Event(Event::Type::SpritePattern, 2);
				case 102:	return Event(Event::Type::SpritePattern, 3);
				case 1268:	case 1334:	case 32:	case 96:	return Event::Type::External;
			}

			if(grauw_index >= 166 && grauw_index < 180) {
				return StandardGenerators::external_every_eight(grauw_index - 166);
			}

			if(grauw_index >= 182 && grauw_index < 1238) {
				const int offset = grauw_index - 182;
				const int block = offset / 32;
				const int sub_block = offset & 31;
				switch(sub_block) {
					case 0:		if(block > 0) return Event(Event::Type::Name, uint8_t(block - 1));		break;
					case 6:		if((sub_block & 3) != 3) return Event::Type::External;					break;
					case 12:	if(block < 32) return Event(Event::Type::SpriteY, uint8_t(block));		break;
					case 18:	if(block > 0) return Event(Event::Type::Pattern, uint8_t(block - 1));	break;
					case 24:	if(block > 0) return Event(Event::Type::Colour, uint8_t(block - 1));	break;
				}
			}

			return std::nullopt;
		}
	};
};

struct YamahaCommandState {
	CommandContext command_context_;
	ModeDescription mode_description_;
	std::unique_ptr<Command> command_ = nullptr;

	enum class CommandStep {
		None,

		CopySourcePixelToStatus,

		ReadSourcePixel,
		ReadDestinationPixel,
		WritePixel,

		ReadSourceByte,
		WriteByte,
	};
	CommandStep next_command_step_ = CommandStep::None;
	int minimum_command_column_ = 0;
	uint8_t command_latch_ = 0;

	void update_command_step(const int current_column) {
		if(!command_) {
			next_command_step_ = CommandStep::None;
			return;
		}
		if(command_->done()) {
			command_ = nullptr;
			next_command_step_ = CommandStep::None;
			return;
		}

		minimum_command_column_ = current_column + command_->cycles;
		switch(command_->access) {
			case Command::AccessType::ReadPoint:
				next_command_step_ = CommandStep::CopySourcePixelToStatus;
			break;

			case Command::AccessType::CopyPoint:
				next_command_step_ = CommandStep::ReadSourcePixel;
			break;
			case Command::AccessType::PlotPoint:
				next_command_step_ = CommandStep::ReadDestinationPixel;
			break;

			case Command::AccessType::WaitForColourReceipt:
				// i.e. nothing to do until a colour is received.
				next_command_step_ = CommandStep::None;
			break;

			case Command::AccessType::CopyByte:
				next_command_step_ = CommandStep::ReadSourceByte;
			break;
			case Command::AccessType::WriteByte:
				next_command_step_ = CommandStep::WriteByte;
			break;
		}
	}
};

// Yamaha-specific storage.
template <Personality personality>
struct Storage<personality, std::enable_if_t<is_yamaha_vdp(personality)>>:
	public YamahaFetcher, public YamahaCommandState
{
	using AddressT = uint32_t;

	// The Yamaha's (optional in real hardware) additional 64kb of expansion RAM.
	// This is a valid target and source for the command engine, but can't be used as a source for current video data.
	std::array<uint8_t, 65536> expansion_ram_;

	// Register indirections.
	int selected_status_ = 0;
	int indirect_register_ = 0;
	bool increment_indirect_register_ = false;

	// Output horizontal and vertical adjustment, plus the selected vertical offset (i.e. hardware scroll).
	int adjustment_[2]{};
	uint8_t vertical_offset_ = 0;

	// The palette, plus a shadow copy in which colour 0 is not the current palette colour 0,
	// but is rather the current global background colour. This simplifies flow when colour 0
	// is set as transparent.
	std::array<uint32_t, 16> palette_{};
	std::array<uint32_t, 16> background_palette_{};
	bool solid_background_ = true;

	// Transient state for palette setting.
	uint8_t new_colour_ = 0;
	uint8_t palette_entry_ = 0;
	bool palette_write_phase_ = false;

	// Recepticle for all five bits of the current screen mode.
	uint8_t mode_ = 0;

	// Used ephemerally during drawing to compound sprites with the 'CC'
	// (compound colour?) bit set.
	uint8_t sprite_cache_[8][32]{};

	// Text blink colours.
	uint8_t blink_text_colour_ = 0;
	uint8_t blink_background_colour_ = 0;

	// Blink state (which is also affects even/odd page display in applicable modes).
	int in_blink_ = 1;
	uint8_t blink_periods_ = 0;
	uint8_t blink_counter_ = 0;

	// Additional things exposed by status registers.
	uint8_t colour_status_ = 0;
	uint16_t colour_location_ = 0;
	uint16_t collision_location_[2]{};
	bool line_matches_ = false;

	Storage() noexcept {
		// Seed to something valid.
		next_event_ = refresh_events.data();
	}

	/// Resets line-ephemeral state for a new line.
	void begin_line(const ScreenMode mode, const bool is_refresh) {
		if(is_refresh) {
			next_event_ = refresh_events.data();
			return;
		}

		switch(mode) {
			case ScreenMode::YamahaText80:
			case ScreenMode::Text:
				next_event_ = text_events.data();
			break;

			case ScreenMode::MultiColour:
			case ScreenMode::YamahaGraphics1:
			case ScreenMode::YamahaGraphics2:
				next_event_ = character_events.data();
			break;

			case ScreenMode::YamahaGraphics3:				// TODO: verify; my guess is that G3 is timed like a bitmap mode
															// in order to fit the pattern for sprite mode 2. Just a guess.
			default:
				next_event_ = sprites_enabled_ ? sprites_events.data() : no_sprites_events.data();
			break;
		}
	}

private:
	static constexpr auto refresh_events = events<RefreshGenerator>();
	static constexpr auto no_sprites_events = events<BitmapGenerator<false>>();
	static constexpr auto sprites_events = events<BitmapGenerator<true>>();
	static constexpr auto text_events = events<TextGenerator>();
	static constexpr auto character_events = events<CharacterGenerator>();
};

// Master System-specific storage.
template <Personality personality> struct Storage<personality, std::enable_if_t<is_sega_vdp(personality)>> {
	using AddressT = uint16_t;

	// The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly
	// fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there
	// isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps
	// track of pending collisions, for visual reproduction.
	struct CRAMDot {
		LineBufferPointer location;
		uint32_t value;
	};
	std::vector<CRAMDot> upcoming_cram_dots_;

	// The Master System's additional colour RAM.
	uint32_t colour_ram_[32];
	bool cram_is_selected_ = false;

	// Programmer-set flags.
	bool vertical_scroll_lock_ = false;
	bool horizontal_scroll_lock_ = false;
	bool hide_left_column_ = false;
	bool shift_sprites_8px_left_ = false;
	bool mode4_enable_ = false;
	uint8_t horizontal_scroll_ = 0;
	uint8_t vertical_scroll_ = 0;

	// Holds the vertical scroll position for this frame; this is latched
	// once and cannot dynamically be changed until the next frame.
	uint8_t latched_vertical_scroll_ = 0;

	// Various resource addresses with VDP-version-specific modifications
	// built in.
	AddressT pattern_name_address_{};
	AddressT sprite_attribute_table_address_{};
	AddressT sprite_generator_table_address_{};

	void begin_line(ScreenMode, bool) {}
};

}
